<?php
// +----------------------------------------------------------------------
// | CoreThink [ Simple Efficient Excellent ]
// +----------------------------------------------------------------------
// | Copyright (c) 2014 http://www.thinkphp.cn/ All rights reserved.
// +----------------------------------------------------------------------
// |
// +----------------------------------------------------------------------
namespace app\attachment\controller;

use app\common\controller\Base;
use GuzzleHttp\Client;
use Tos\Exception\TosException;
use Tos\Model\CompleteMultipartUploadInput;
use Tos\Model\CreateMultipartUploadInput;
use Tos\Model\DeleteObjectInput;
use Tos\Model\HeadBucketInput;
use Tos\Model\ListObjectsInput;
use Tos\Model\PreSignedURLInput;
use Tos\Model\UploadedPart;
use Tos\Model\UploadPartInput;
use Tos\TosClient;
use Tos\Exception\TosClientException;
use Tos\Exception\TosServerException;
use Tos\Model\PutObjectInput;
use Tos\Model\Enum;
use util\Logger;

/**
 * 阿里云存储对象上传
 * @author nagi
 */
class DouyinUpload extends Base
{
    private $bucket;
    private $tosClient;

    public function __construct($driver)
    {
        try {
            $this->bucket    = $driver['bucket'];
            $this->tosClient = new \Tos\TosClient([
                'region'   => $driver['endpoint'],
                'endpoint' => "tos-{$driver['endpoint']}.volces.com",
                'ak'       => $driver['accessKey'],
                'sk'       => $driver['secretKey'],
            ]);
        } catch (TosClientException $e) {
            trace($e->getMessage(), 'error');
            return;
        }
    }

    /**
     * 上传
     * @param null $info
     * @return array
     */
    public function upload($info = null)
    {
        if (empty($info)) {
            $info = $this->dealFiles($_FILES);
            $info = $info['file'];
        }

        try {
            //文件名
            $object = $info['md5'] . '.' . $info['ext'];
//            $object = $info['name'];
            //文件
            $filepath = $info['tmp_name'];
            //存储目录
            $date = date('Y-m-d', time());
            if (str_starts_with($info['type'], 'image/')) {
                $path = 'images/' . $date;
            } elseif (str_starts_with($info['type'], 'video/')) {
                $path = 'videos/' . $date;
            } else {
                $path = $date;
            }
            //上传文件
            $objectKey = $path . '/' . $object;
            $size      = $info['size'];
            if ($size > config('upload_slice_size')) {
                $input = new CreateMultipartUploadInput($this->bucket, $objectKey);
            } else {
                $input = new PutObjectInput($this->bucket, $objectKey, fopen($filepath, 'r'));
            }
            // 设置对象 ACL
            $input->setACL(Enum::ACLPublicRead); // 设置公共读权限
            $input->setContentType(mime_content_type($filepath));

            if ($size > config('upload_slice_size')) {
                // 创建分片上传任务
                $input = new CreateMultipartUploadInput($this->bucket, $objectKey);
                // 设置对象 StorageClass
                $input->setStorageClass(Enum::StorageClassStandard);
                $output = $this->tosClient->createMultipartUpload($input);
                // 获取 UploadID
                $uploadId = $output->getUploadID();
                // 上传多个分片
                $partSize  = intval(config('upload_slice_part')); //每个分片大小
                $fileSize  = $info['size'];
                $partCount = intval($fileSize / $partSize);
                if (($lastPartSize = $fileSize % $partSize) !== 0) {
                    $partCount++;
                } else {
                    $lastPartSize = $partSize;
                }

                $parts = [];
                for ($i = 0; $i < $partCount; $i++) {
                    $partNumber = $i + 1;
                    $file       = fopen($filepath, 'r');
                    // 设置当前上传的文件起始位置
                    fseek($file, $partSize * $i, 0);
                    $input = new UploadPartInput($this->bucket, $objectKey, $uploadId, $partNumber);
                    if ($i === $partCount - 1) {
                        // 处理最后一个分片
                        $input->setContentLength($lastPartSize);
                    } else {
                        $input->setContentLength($partSize);
                    }
                    $input->setContent($file);
                    $output = $this->tosClient->uploadPart($input);
                    if (is_resource($file)) {
                        fclose($file);
                    }
                    // 收集所有分片
                    $parts[] = new UploadedPart($partNumber, $output->getETag());
                }
                // 合并分片
                $input  = new CompleteMultipartUploadInput($this->bucket, $objectKey, $uploadId, $parts);
                $output = $this->tosClient->completeMultipartUpload($input);
            } else {
                $output = $this->tosClient->putObject($input);
            }
            $result = ['code' => 0, 'msg' => '上传成功', 'name' => $object, 'url' => $objectKey];
        } catch (\Exception $e) {
            trace($e->getMessage(), 'error');
            $result = ['code' => -1, 'msg' => '上传失败', 'data' => null];
        } finally {
            if (is_resource($filepath)) {
                fclose($filepath);
            }
        }

        return $result;
    }

    /**
     * 上传本地文件
     * @param string $filename 文件名
     * @param string $filepath 文件目录
     * @param string $file 本地文件
     * @return string
     * @throws \Exception
     */
    public function upload_local(string $filename, string $filepath, string $file)
    {
        try {
            //文件名
            $object = $filename;
            $type   = mime_content_type($file);
            //存储目录
            if (str_starts_with($type, 'image/')) {
                $path = 'images/' . $filepath;
            } elseif (str_starts_with($type, 'video/')) {
                $path = 'videos/' . $filepath;
            } else {
                $path = $filepath;
            }
            //上传文件
            $oss_file = $path . '/' . $object;
            $size     = filesize($file);
            if ($size > config('upload_slice_size')) {
                $input = new CreateMultipartUploadInput($this->bucket, $oss_file);
            } else {
                $input = new PutObjectInput($this->bucket, $oss_file, fopen($file, 'r'));
            }
            // 设置对象 ACL
            $input->setACL(Enum::ACLPublicRead); // 设置公共读权限
            $input->setContentType(mime_content_type($file));

            if ($size > config('upload_slice_size')) {
                // 创建分片上传任务
                $input = new CreateMultipartUploadInput($this->bucket, $oss_file);
                // 设置对象 StorageClass
                $input->setStorageClass(Enum::StorageClassStandard);
                $output = $this->tosClient->createMultipartUpload($input);
                // 获取 UploadID
                $uploadId = $output->getUploadID();
                // 上传多个分片
                $partSize  = intval(config('upload_slice_part')); //每个分片大小
                $fileSize  = $info['size'];
                $partCount = intval($fileSize / $partSize);
                if (($lastPartSize = $fileSize % $partSize) !== 0) {
                    $partCount++;
                } else {
                    $lastPartSize = $partSize;
                }

                $parts = [];
                for ($i = 0; $i < $partCount; $i++) {
                    $partNumber = $i + 1;
                    $part_file  = fopen($file, 'r');
                    // 设置当前上传的文件起始位置
                    fseek($part_file, $partSize * $i, 0);
                    $input = new UploadPartInput($this->bucket, $oss_file, $uploadId, $partNumber);
                    if ($i === $partCount - 1) {
                        // 处理最后一个分片
                        $input->setContentLength($lastPartSize);
                    } else {
                        $input->setContentLength($partSize);
                    }
                    $input->setContent($part_file);
                    $output = $this->tosClient->uploadPart($input);
                    if (is_resource($part_file)) {
                        fclose($part_file);
                    }
                    // 收集所有分片
                    $parts[] = new UploadedPart($partNumber, $output->getETag());
                }
                // 合并分片
                $input  = new CompleteMultipartUploadInput($this->bucket, $oss_file, $uploadId, $parts);
                $output = $this->tosClient->completeMultipartUpload($input);
            } else {
                $output = $this->tosClient->putObject($input);
            }
            return $oss_file;
        } catch (\Exception $e) {
            Logger::error('上传失败：' . $e->getMessage());
            throw new \Exception('上传失败');
        } finally {
            if (is_resource($file)) {
                fclose($file);
            }
        }
    }

    /**
     * 删除文件
     * @param string $object 对象名
     * @param string $driver 上传驱动
     * @return array
     * @throws \think\Exception
     * @throws \think\exception\PDOException
     */
    public function delete($object = '', $driver = '')
    {
        if (empty($object)) {
            $object = $this->request->param('object');
        }
        try {
            // 删除单个对象
            $input  = new DeleteObjectInput($this->bucket, $object);
            $output = $this->tosClient->deleteObject($input);
            //删除附件记录
            deleteModel('attachment', ['path' => $object, 'driver' => $driver]);
            $result = ['code' => 0, 'msg' => '删除成功'];
        } catch (TosServerException  $e) {
            trace($e->getMessage(), 'error');
            $result = ['code' => -1, 'msg' => '删除失败'];
        }
        return $result;
    }

    /**
     * 转换上传文件数组变量为正确的方式
     * @access private
     * @param array $files 上传的文件变量
     * @return array
     */
    private function dealFiles($files)
    {
        $fileArray = array();
        $n         = 0;
        foreach ($files as $key => $file) {
            if (is_array($file['name'])) {
                $keys  = array_keys($file);
                $count = count($file['name']);
                for ($i = 0; $i < $count; $i++) {
                    $fileArray[$n]['key'] = $key;
                    foreach ($keys as $_key) {
                        $fileArray[$n][$_key] = $file[$_key][$i];
                    }
                    $n++;
                }
            } else {
                $fileArray = $files;
                break;
            }
        }
        return $fileArray;
    }

    /**
     * 获取全部文件夹或目标文件夹
     * @param string $prefix
     * @param string $delimiter
     * @param string $targetName 目标文件名称
     * @return string|array|null
     * @throws \Exception
     */
    public function getAllFolders(string $prefix, string $delimiter, string $targetName = '')
    {
        $folders = [];
        $marker  = ''; // 分页标记

        do {
            try {
                // 构建 ListObjectsInput 参数
                $input = new ListObjectsInput();
                $input->setBucket($this->bucket);
                $input->setPrefix($prefix);
                $input->setDelimiter($delimiter);
                $input->setMarker($marker);

                // 列出对象
                $result = $this->tosClient->listObjects($input);

                // 提取当前层级的文件夹
                $commonPrefixes = $result->getCommonPrefixes();
                if (!empty($commonPrefixes)) {
                    foreach ($commonPrefixes as $commonPrefix) {
                        $folderPath = $commonPrefix->getPrefix();

                        // 如果传入了对象名，并且文件夹路径包含该名字，则直接返回该路径
                        if ($targetName && str_contains($folderPath, $targetName)) {
                            return $folderPath;
                        }

                        $folders[] = $folderPath;

                        // 递归获取子文件夹
                        $subFolders = $this->getAllFolders($folderPath, $delimiter, $targetName);
                        if ($targetName && $subFolders !== null) {
                            return $subFolders; // 如果找到目标文件夹，直接返回
                        }
                        $folders = array_merge($folders, (array)$subFolders);
                    }
                }
                // 更新分页标记
                $marker = $result->getNextMarker();
            } catch (TosException $e) {
                throw new \Exception("抖音云对象存储异常: " . $e->getMessage());
            }
        } while ($result->isTruncated()); // 是否还有更多数据
        // 如果传入了对象名但未找到匹配的文件夹，返回 null
        return $targetName ? null : $folders;
    }

    /**
     * 获取某个文件夹下的全部文件
     * @param string $folderPath 文件路径
     * @param null $suffix 文件后缀
     * @return array
     */
    public function getFiles(string $folderPath, $suffix = null)
    {
        $prefix    = rtrim($folderPath, '/') . '/'; // 确保文件夹路径以斜杠结尾
        $delimiter = '/'; // 使用斜杠作为文件夹分隔符

        try {
            // 构建 ListObjectsInput 参数
            $input = new ListObjectsInput();
            $input->setBucket($this->bucket);
            $input->setPrefix($prefix); // 设置文件夹路径
            $input->setDelimiter($delimiter);
            // 列出对象
            $result = $this->tosClient->listObjects($input);
            // 提取文件列表
            $files   = [];
            $objects = $result->getContents();
            if (!empty($objects)) {
                foreach ($objects as $object) {
                    $fileKey = $object->getKey();
                    // 如果传入了后缀，则检查文件是否匹配
                    if ($suffix) {
                        if (str_ends_with($fileKey, $suffix)) {
                            $files[] = [
                                'key'           => $fileKey,
                                'size'          => $object->getSize(),
                                'last_modified' => $object->getLastModified(),
                            ];
                        }
                    } else {
                        // 如果没有传入后缀，则返回所有文件
                        $files[] = [
                            'key'           => $fileKey,
                            'size'          => $object->getSize(),
                            'last_modified' => $object->getLastModified(),
                        ];
                    }
                }
            }
            return $files;
        } catch (TosException $e) {
            Logger::error('获取文件列表失败:' . $e->getMessage());
            return [];
        }
    }

}
